"""
Cookie Security Tests

Tests for the dynamic cookie security behavior that allows private network HTTP
while requiring HTTPS for public internet connections.

Security model:
- Private network HTTP (localhost, LAN IPs): Cookies work without Secure flag
- Local proxy (nginx on localhost/LAN): Cookies work without Secure flag
- Public proxy (Cloudflare, AWS): Get Secure flag (requires HTTPS)
- Public IP HTTP: Get Secure flag (will fail, by design - requires HTTPS)
- HTTPS: Always get Secure flag
- TESTING mode: Never get Secure flag (for CI/development)

Key insight: We check the ACTUAL connection IP (REMOTE_ADDR), not X-Forwarded-For.
SecureCookieMiddleware is the outer wrapper, so it sees the original REMOTE_ADDR
before ProxyFix modifies it. This means:
- Local proxy (nginx on 127.0.0.1) → REMOTE_ADDR=127.0.0.1 (private) → HTTP works
- Public proxy (Cloudflare) → REMOTE_ADDR=104.16.x.x (public) → requires HTTPS
- Spoofed X-Forwarded-For from public IP → still blocked (REMOTE_ADDR is public)

Private IPs (RFC 1918) include: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, plus localhost.
This matches the behavior of self-hosted apps like Jellyfin and Home Assistant.
"""

import pytest
from tests.test_utils import add_src_to_path

add_src_to_path()


@pytest.fixture
def app():
    """Create test application with TESTING mode enabled (default for tests)."""
    from src.local_deep_research.web.app_factory import create_app

    app, _ = create_app()
    app.config["TESTING"] = True
    app.config["WTF_CSRF_ENABLED"] = False  # Disable CSRF for these tests
    # LDR_TESTING_MODE is set by create_app based on env vars
    # For most tests, this is True (CI environment)
    return app


@pytest.fixture
def app_production_mode():
    """Create test application with production-like cookie security."""
    import os
    from src.local_deep_research.web.app_factory import create_app

    # Temporarily unset ALL testing env vars to simulate production
    # Note: PYTEST_CURRENT_TEST is also checked by create_app
    old_testing = os.environ.pop("TESTING", None)
    old_ci = os.environ.pop("CI", None)
    old_pytest = os.environ.pop("PYTEST_CURRENT_TEST", None)

    try:
        app, _ = create_app()
        app.config["TESTING"] = True  # Flask testing mode for test client
        app.config["WTF_CSRF_ENABLED"] = False
        # Explicitly disable LDR testing mode to test production cookie behavior
        app.config["LDR_TESTING_MODE"] = False
        # Set to http to properly test HTTP behavior (default is https for URL generation)
        # In real production with HTTPS, wsgi.url_scheme is set by the server
        app.config["PREFERRED_URL_SCHEME"] = "http"
        return app
    finally:
        # Restore env vars
        if old_testing is not None:
            os.environ["TESTING"] = old_testing
        if old_ci is not None:
            os.environ["CI"] = old_ci
        if old_pytest is not None:
            os.environ["PYTEST_CURRENT_TEST"] = old_pytest


@pytest.fixture
def client(app):
    """Create test client."""
    return app.test_client()


class TestLocalhostCookieSecurity:
    """Test that localhost HTTP connections work without Secure flag."""

    def test_localhost_http_no_secure_flag(self, client):
        """Localhost HTTP requests should NOT have Secure flag on cookies."""
        # Default test client simulates localhost (127.0.0.1)
        response = client.get("/auth/login")

        # Check Set-Cookie header
        set_cookie = response.headers.get("Set-Cookie", "")

        # Should NOT have Secure flag for localhost HTTP
        # Note: In TESTING mode, Secure is never added
        assert response.status_code == 200
        # The cookie should be set (session cookie)
        assert "session=" in set_cookie

    def test_localhost_session_cookie_works(self, client):
        """Verify session cookies are sent back on subsequent requests from localhost."""
        # Get login page (establishes session)
        response1 = client.get("/auth/login")
        assert response1.status_code == 200

        # Make another request - session should persist
        response2 = client.get("/auth/login")
        assert response2.status_code == 200

        # Session should be maintained (cookie sent back)
        # This would fail if Secure flag was set but we're on HTTP


class TestLocalhostProductionMode:
    """Test localhost HTTP works in production mode (non-testing)."""

    def test_localhost_http_no_secure_flag_in_production(
        self, app_production_mode
    ):
        """Localhost HTTP requests should NOT have Secure flag even in production mode."""
        app = app_production_mode

        with app.test_client() as client:
            # Direct localhost request (no X-Forwarded-For)
            response = client.get("/auth/login")

            set_cookie = response.headers.get("Set-Cookie", "")

            # Should NOT have Secure flag for direct localhost HTTP
            assert "; Secure" not in set_cookie, (
                f"Localhost HTTP should NOT have Secure flag. Got: {set_cookie}"
            )
            assert "session=" in set_cookie  # Cookie should still be set


class TestLANIPCookieSecurity:
    """Test that LAN/private network IPs work without Secure flag (RFC 1918)."""

    def test_lan_ip_192_168_no_secure_flag(self, app_production_mode):
        """192.168.x.x LAN IPs should NOT have Secure flag."""
        app = app_production_mode

        with app.test_client() as client:
            # Simulate request from 192.168.1.100 (common home network)
            response = client.get(
                "/auth/login",
                environ_base={"REMOTE_ADDR": "192.168.1.100"},
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            # Should NOT have Secure flag for LAN IP over HTTP
            assert "; Secure" not in set_cookie, (
                f"LAN IP 192.168.x.x should NOT have Secure flag. Got: {set_cookie}"
            )
            assert "session=" in set_cookie  # Cookie should still be set

    def test_lan_ip_10_x_no_secure_flag(self, app_production_mode):
        """10.x.x.x LAN IPs should NOT have Secure flag."""
        app = app_production_mode

        with app.test_client() as client:
            # Simulate request from 10.0.0.50 (common corporate network)
            response = client.get(
                "/auth/login",
                environ_base={"REMOTE_ADDR": "10.0.0.50"},
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            assert "; Secure" not in set_cookie, (
                f"LAN IP 10.x.x.x should NOT have Secure flag. Got: {set_cookie}"
            )

    def test_lan_ip_172_16_no_secure_flag(self, app_production_mode):
        """172.16-31.x.x LAN IPs should NOT have Secure flag."""
        app = app_production_mode

        with app.test_client() as client:
            # Simulate request from 172.16.0.1
            response = client.get(
                "/auth/login",
                environ_base={"REMOTE_ADDR": "172.16.0.1"},
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            assert "; Secure" not in set_cookie, (
                f"LAN IP 172.16.x.x should NOT have Secure flag. Got: {set_cookie}"
            )

    def test_public_ip_gets_secure_flag(self, app_production_mode):
        """Public IPs (non-RFC 1918) should get Secure flag over HTTP."""
        app = app_production_mode

        with app.test_client() as client:
            # Simulate request from a public IP (Google DNS)
            response = client.get(
                "/auth/login",
                environ_base={"REMOTE_ADDR": "8.8.8.8"},
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            # Public IP should have Secure flag
            assert "; Secure" in set_cookie, (
                f"Public IP should have Secure flag. Got: {set_cookie}"
            )


class TestLocalProxySecurity:
    """Test that local proxies (nginx on localhost/LAN) work without Secure flag."""

    def test_local_proxy_no_secure_flag(self, app_production_mode):
        """Local proxy (127.0.0.1) with X-Forwarded-For should NOT have Secure flag."""
        app = app_production_mode

        with app.test_client() as client:
            # Proxy on localhost, client behind it
            # REMOTE_ADDR is the proxy's IP (127.0.0.1 = private)
            response = client.get(
                "/auth/login",
                headers={"X-Forwarded-For": "192.168.1.100"},
                environ_base={"REMOTE_ADDR": "127.0.0.1"},
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            # Should NOT have Secure flag - proxy is on localhost (private)
            assert "; Secure" not in set_cookie, (
                f"Local proxy should NOT have Secure flag. Got: {set_cookie}"
            )

    def test_lan_proxy_no_secure_flag(self, app_production_mode):
        """LAN proxy (192.168.x.x) with X-Forwarded-For should NOT have Secure flag."""
        app = app_production_mode

        with app.test_client() as client:
            # Proxy on LAN, client could claim any IP
            response = client.get(
                "/auth/login",
                headers={
                    "X-Forwarded-For": "8.8.8.8"
                },  # Client claims public IP
                environ_base={
                    "REMOTE_ADDR": "192.168.1.1"
                },  # But proxy is on LAN
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            # Should NOT have Secure flag - proxy is on LAN (private)
            assert "; Secure" not in set_cookie, (
                f"LAN proxy should NOT have Secure flag. Got: {set_cookie}"
            )

    def test_public_proxy_gets_secure_flag(self, app_production_mode):
        """Public proxy (Cloudflare, AWS) should get Secure flag."""
        app = app_production_mode

        with app.test_client() as client:
            # Public proxy (e.g., Cloudflare IP)
            response = client.get(
                "/auth/login",
                headers={
                    "X-Forwarded-For": "192.168.1.100"
                },  # Client claims LAN
                environ_base={
                    "REMOTE_ADDR": "104.16.0.1"
                },  # Proxy is public IP
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            # Public proxy should have Secure flag
            assert "; Secure" in set_cookie, (
                f"Public proxy should have Secure flag. Got: {set_cookie}"
            )

    def test_spoofed_xff_from_public_ip_blocked(self, app_production_mode):
        """Spoofed X-Forwarded-For from public IP should still be blocked."""
        app = app_production_mode

        with app.test_client() as client:
            # Attacker tries to spoof private IP in X-Forwarded-For header
            response = client.get(
                "/auth/login",
                headers={"X-Forwarded-For": "127.0.0.1"},  # Spoofed localhost
                environ_base={
                    "REMOTE_ADDR": "8.8.8.8"
                },  # But actual IP is public
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            # Should still have Secure flag - we check REMOTE_ADDR, not header
            assert "; Secure" in set_cookie, (
                f"Spoofed X-Forwarded-For from public IP should be blocked. Got: {set_cookie}"
            )


class TestTestingModeBehavior:
    """Test that TESTING mode disables Secure flag entirely."""

    def test_testing_mode_no_secure_flag(self, app):
        """In TESTING mode, Secure flag should never be added."""
        app.config["LDR_TESTING_MODE"] = True

        with app.test_client() as client:
            # Even with X-Forwarded-For, TESTING mode skips Secure
            response = client.get(
                "/auth/login",
                headers={"X-Forwarded-For": "192.168.1.100"},
            )

            set_cookie = response.headers.get("Set-Cookie", "")

            # In TESTING mode, no Secure flag
            assert "; Secure" not in set_cookie


class TestCSRFErrorMessage:
    """Test that CSRF errors provide helpful messages for cookie issues."""

    def test_csrf_error_message_is_helpful(self, app):
        """CSRF error for non-localhost HTTP should explain the issue."""
        app.config["WTF_CSRF_ENABLED"] = True
        app.config["LDR_TESTING_MODE"] = False

        with app.test_client() as client:
            # Get login page to establish session
            client.get("/auth/login")

            # Try to POST without proper CSRF token
            # Simulating non-localhost by adding X-Forwarded-For
            response = client.post(
                "/auth/login",
                data={"username": "test", "password": "test"},
                headers={"X-Forwarded-For": "192.168.1.100"},
            )

            # Should get a 400 error with helpful message
            assert response.status_code == 400

            # Check for helpful error message
            data = response.get_json()
            if data and "error" in data:
                error_msg = data["error"]
                # Should mention the security issue
                assert (
                    "Session cookie error" in error_msg or "CSRF" in error_msg
                )


class TestCookieSecurityDocumentation:
    """Documentation tests explaining the security model."""

    def test_security_model_documentation(self):
        """
        Document the cookie security model.

        WHY THIS EXISTS:
        - Session cookies with Secure flag only work over HTTPS
        - Local/LAN development typically uses HTTP
        - We want security for public internet but usability for private networks
        - Matches behavior of self-hosted apps like Jellyfin, Home Assistant

        HOW IT WORKS:
        1. Check the ACTUAL connection IP (REMOTE_ADDR), not X-Forwarded-For
        2. SecureCookieMiddleware is outer wrapper, sees original REMOTE_ADDR
        3. If REMOTE_ADDR is private (RFC 1918): Skip Secure flag
           - This includes both direct clients AND local proxy servers
        4. If REMOTE_ADDR is public: Add Secure flag (requires HTTPS)
        5. TESTING mode: Never add Secure flag (CI/test environments)

        SECURITY PROPERTIES:
        - Localhost HTTP: ✅ Works - traffic is local only
        - LAN HTTP (192.168.x.x, 10.x.x.x, 172.16-31.x.x): ✅ Works - private network
        - Local proxy (nginx on localhost/LAN): ✅ Works - proxy has private IP
        - Public proxy (Cloudflare, AWS): ✅ Blocked - proxy has public IP
        - Public IP HTTP: ✅ Blocked - requires HTTPS
        - X-Forwarded-For spoofing: ✅ Blocked - we check REMOTE_ADDR, not header
        - TESTING mode: ⚠️ Insecure but explicit opt-in

        ATTACK PREVENTION:
        - Session hijacking on public networks: Prevented by Secure flag
        - X-Forwarded-For spoofing: Prevented by checking REMOTE_ADDR (actual IP)
        - Private network convenience: Preserved for RFC 1918 IPs
        """
        assert True  # Documentation test


def test_cookie_security_summary():
    """
    Summary of cookie security behavior for CI validation.

    Expected behavior:
    | Scenario                      | REMOTE_ADDR   | Secure Flag | Works? |
    |-------------------------------|---------------|-------------|--------|
    | localhost:5000 (direct)       | 127.0.0.1     | No          | ✅ Yes |
    | LAN client (direct)           | 192.168.x.x   | No          | ✅ Yes |
    | Local proxy (nginx)           | 127.0.0.1     | No          | ✅ Yes |
    | LAN proxy (nginx on LAN)      | 192.168.x.x   | No          | ✅ Yes |
    | Public proxy (Cloudflare)     | 104.16.x.x    | Yes         | ❌ No  |
    | Public IP (direct)            | 8.8.8.8       | Yes         | ❌ No  |
    | Spoofed XFF from public IP    | 8.8.8.8       | Yes         | ❌ No  |
    | TESTING=1 mode                | any           | No          | ✅ Yes |

    Note: We check REMOTE_ADDR (actual connection IP), not X-Forwarded-For.
    This allows local proxies while blocking spoofing attacks from public IPs.
    """
    assert True  # Documentation/summary test
